﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;

namespace FileExplorerService
{
    using System.Collections;
    using System.IO;
    using System.Windows;

    // HINWEIS: Mit dem Befehl "Umbenennen" im Menü "Umgestalten" können Sie den Klassennamen "Service1" sowohl im Code als auch in der SVC- und der Konfigurationsdatei ändern.
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] 
    public class FileExchangeService : IFileExchangeService
    {
        private SortedDictionary<Guid, FileSession> fileSessions =new SortedDictionary<Guid, FileSession>();
        private SortedDictionary<Guid, EnumerationSession> enumerationSessions =new SortedDictionary<Guid, EnumerationSession>();

        public FileExchangeService(string currentDirectory = null)
        {
            if (string.IsNullOrEmpty(currentDirectory))
            {
                currentDirectory = Environment.CurrentDirectory;
            }

            this.CurrentDownloadDirectory = currentDirectory;
            this.CurrentUploadDirectory = currentDirectory;
        }

        public string CurrentDownloadDirectory { get; set; }
        public string CurrentUploadDirectory { get; set; }

        public event EventHandler<StartedEventArgs> StartEvent;
        public event EventHandler<EventArgs> EndEvent;
        public event Action<Exception> ErrorEvent;

        public void InvokeErrorEvent(Exception error)
        {
            Action<Exception> handler = this.ErrorEvent;
            if (handler != null)
            {
                handler(error);
            }
        }

        public void InvokeEndEvent(EventArgs e)
        {
            EventHandler<EventArgs> handler = this.EndEvent;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        public event EventHandler<SendDataEventArgs> SendDataEvent;
        public event EventHandler<SendDataEventArgs> ReadDataEvent;

        public void InvokeReadDataEvent(SendDataEventArgs e)
        {
            EventHandler<SendDataEventArgs> handler = this.ReadDataEvent;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        public void InvokeSendDataEvent(SendDataEventArgs e)
        {
            EventHandler<SendDataEventArgs> handler = this.SendDataEvent;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        public void InvokeStartEvent(StartedEventArgs e)
        {
            EventHandler<StartedEventArgs> handler = this.StartEvent;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        public Guid Start(string path, bool read)
        {
            if (path.StartsWith("\\"))
            {
                path = path.Substring(1);
            }
            try
            {
                Guid guid;

                do
                {
                    guid = Guid.NewGuid();
                }
                while (this.fileSessions.ContainsKey(guid) && this.fileSessions[guid].IsValid);
                if (this.fileSessions.ContainsKey(guid))
                {
                    this.fileSessions.Remove(guid);
                }
                var dir = read ? this.CurrentUploadDirectory : this.CurrentDownloadDirectory;
                this.fileSessions.Add(guid, new FileSession(Path.Combine(dir, path), read));
                this.InvokeStartEvent(new StartedEventArgs(path, read));
                return guid;
            }
            catch (Exception e)
            {
                InvokeErrorEvent(e);
                throw;
            }
        }

        public Guid StartEnumeration(string path)
        {
            if (path.StartsWith("\\"))
            {
                path = path.Substring(1);
            }
            try
            {
                Guid guid;

                do
                {
                    guid = Guid.NewGuid();
                }
                while (this.enumerationSessions.ContainsKey(guid) && this.enumerationSessions[guid].IsValid);
                if (this.enumerationSessions.ContainsKey(guid))
                {
                    this.enumerationSessions.Remove(guid);
                }

                Directory.CreateDirectory(this.CurrentUploadDirectory);
                this.enumerationSessions.Add(guid, new EnumerationSession(Path.Combine(this.CurrentUploadDirectory, path)));
                return guid;
            }
            catch (Exception e)
            {
                InvokeErrorEvent(e);
                throw;
            }
        }

        public void SendData(Guid handle, byte[] bytes, int offset, int count)
        {
            try
            {
                var session = this.CheckFileSession(handle);

                session.SendData(bytes, offset, count);
                this.InvokeSendDataEvent(new SendDataEventArgs(count));
            }
            catch (Exception e)
            {
                InvokeErrorEvent(e);
                throw;
            }
        }

        private FileSession CheckFileSession(Guid handle)
        {
            if (!this.fileSessions.ContainsKey(handle))
            {
                throw new ArgumentException("Invalid Session");
            }
            
            var session = this.fileSessions[handle];
            if (!session.IsValid)
            {
                this.fileSessions.Remove(handle);
                throw new ArgumentException("Outdated Session");
            }

            return session;
        }

        public byte[] ReadData(Guid handle, int maxBuffersize)
        {

            var session = this.CheckFileSession(handle);
            var bytes = new byte[maxBuffersize];
            var read = session.ReadData(bytes, 0, maxBuffersize);
            var copy = new byte[read];
            Array.Copy(bytes, copy, read);
            this.InvokeReadDataEvent(new SendDataEventArgs(maxBuffersize));
            return copy;

        }

        public FileElements GetNext(Guid handle)
        {
            var session = this.CheckEnumerateSession(handle);
            var next = session.GetNext();
            if (!next.IsMore)
            {
                session.IsValid = false;
                enumerationSessions.Remove(handle);
            }

            return next;
        }

        private EnumerationSession CheckEnumerateSession(Guid handle)
        {
            if (!this.enumerationSessions.ContainsKey(handle))
            {
                throw new ArgumentException("Invalid Session");
            }

            var session = this.enumerationSessions[handle];
            if (!session.IsValid)
            {
                session.IsValid = false;
                this.enumerationSessions.Remove(handle);
                throw new ArgumentException("Outdated Session");
            }
            return session;
        }

        public void End(Guid handle)
        {
            try
            {
                var session = this.CheckFileSession(handle);
                session.Close();
                this.fileSessions.Remove(handle);
                InvokeEndEvent(EventArgs.Empty);
            }
            catch (Exception e)
            {
                InvokeErrorEvent(e);
                throw;
            }
           
        }
    }

    internal class EnumerationSession : Session
    {
        private const int MaxFetch = 20;

        private readonly string path;

        private IEnumerator<string> enumerator;

        public EnumerationSession(string path)
        {
            this.path = path;
            this.enumerator = Directory.GetFileSystemEntries(path).Cast<string>().GetEnumerator();
        }

        public FileElements GetNext()
        {
            this.UsingSession();
            List<FileElement> elements = new List<FileElement>(MaxFetch);
            for (int i = 0; i < MaxFetch; i++)
            {
                if (!enumerator.MoveNext())
                {
                    return new FileElements() { Content = elements, IsMore = false };
                }

                var current = enumerator.Current;
                bool isFile = File.Exists(current);
                elements.Add(new FileElement(){IsFile = isFile, Name = Path.GetFileName(current)});
            }

            return new FileElements(){Content = elements, IsMore = true};
        }
    }

    public class SendDataEventArgs : EventArgs
    {
        public SendDataEventArgs(int byteCount)
        {
            this.ByteCount = byteCount;
        }

        public int ByteCount { get; private set; }
    }

    public class StartedEventArgs : EventArgs
    {
        public StartedEventArgs(string path, bool read)
        {
            this.Path = path;
            this.Read = read;
        }

        public string Path { get; private set; }
        public bool Read { get; private set; }
    }

    internal class Session
    {
        private static TimeSpan validSessionTime = TimeSpan.FromMinutes(20);
        private DateTime lastUse = DateTime.Now;

        public bool IsValid
        {
            get
            {
                return DateTime.Now - this.lastUse <= validSessionTime;
            }
            set
            {
                if (!value)
                {
                    lastUse = DateTime.Now - (validSessionTime + TimeSpan.FromMinutes(1));
                }
            }
        }

        protected void UsingSession()
        {
            if (!IsValid)
            {
                throw new InvalidOperationException("Session already invalid");
            }

            lastUse = DateTime.Now;
        }
    }

    internal class FileSession : Session
    {
        private readonly string path;
        
        private FileStream stream;

        public FileSession(string path, bool read)
        {
            this.path = path;
            var dir = Path.GetDirectoryName(path);
            if (!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }
            this.stream = File.Open(path, read ? FileMode.Open : FileMode.Create, read ? FileAccess.Read : FileAccess.Write);
        }

        public void SendData(byte[] bytes, int offset, int count)
        {
            this.UsingSession();
            stream.Write(bytes, offset, count);
        }

        
        public int ReadData(byte[] bytes, int offset, int count)
        {
            this.UsingSession();
            return stream.Read(bytes, offset, count);
        }

        public void Close()
        {
            stream.Close();
            stream.Dispose();
        }
    }
}
